/* =============================================================
   EGE NALEGKE — упрощённый партнёрский кабинет для учеников
   Версия 2.4 (08.09.2026) — защита от редактора темы, промокод
   с правильного источника, настоящее сохранение выбора выплаты
   -------------------------------------------------------------
   &#9888;&#65039; ЧТО ИЗМЕНИЛОСЬ ПО СРАВНЕНИЮ С ТЕМ, ЧТО СЕЙЧАС НА БОЕВОМ.
   Изменены ТРИ места, все — по одной причине. Логика не тронута.

   На боевом файл НЕ ПАРСИТСЯ ВООБЩЕ: в консоли
   «SyntaxError: Invalid or unexpected token». Функция esc там
   выглядит так:

       return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];

   Последняя пара — три апострофа подряд. В исходнике стояли
   HTML-сущности, записанные буквально, и редактор темы GetCourse
   их РАСКОДИРОВАЛ при сохранении. Для четырёх из пяти это
   безобидно, а сущность апострофа превратилась в сам апостроф
   внутри апострофов — и файл перестал читаться целиком. Поэтому
   не собирался блок, поэтому CSS рефералки нечего было оформлять.

   &#9888;&#65039; Тот же редактор двумя днями раньше испортил эмодзи в CSS
   плашки «важно». Правило общее: в файлах темы НЕЛЬЗЯ писать
   HTML-сущности и эмодзи буквально. Пишем кодами \uXXXX — они
   состоят из латиницы и цифр, трогать там нечего.

   Три места:
     1. esc()      — амперсанд записан кодом \u0026;
     2. баннер     — сущность &#10024; заменена кодом \u2728;
     3. блок бонусов — сущность &#127873; заменена парой кодов.

   Вывод на экране от этого не меняется ни на пиксель: \u0026 —
   это тот же символ «и коммерческое», просто собирается уже в
   браузере, и раскодировать в исходнике становится нечего.
   -------------------------------------------------------------
   Принципы (без изменений):
   1. Страница определяется по window.controllerId / window.actionId,
      а не по URL-подстроке. Админские разделы (actionId = index,
      user, settings, commissions...) не затрагиваются вообще.
   2. Скрытием старого управляет ЭТОТ скрипт, а не CSS. Класс на
      <html> ставится только после того, как новый блок собран.
      Не собрался — возвращается штатный кабинет GetCourse.
   3. #userStat в GetCourse грузится через AJAX уже ПОСЛЕ загрузки
      страницы, поэтому данные ждём, а не читаем сразу.
   4. Аварийный просмотр оригинала: ?original=1
      Предпросмотр ученического вида админом: ?student=1

   &#9888;&#65039; АДМИН ВСЕГДА ВИДИТ СТАРЫЙ КАБИНЕТ (skipAdmins: true).
   Чтобы проверить правку, открывать с ?student=1.
   ============================================================= */
(function () {
  'use strict';

  var CFG = {
    cls:        'egen-simple-partner',
    rulesUrl:   '/referalka',
    timeoutMs:  8000,      // сколько ждём догрузки статистики
    skipAdmins: true,      // админам всегда показывать оригинал

    /* ГДЕ БРАТЬ ПРОМОКОД.
       Переменную {promo_code_bublik} нельзя написать прямо в этом
       файле: файлы оформления GetCourse отдаёт как обычную статику,
       один и тот же файл всем пользователям, и переменные в нём НЕ
       подставляются. Проверено: в вашем же файле «оформление
       уроков.js» плейсхолдеры {title} и {percent} приходят как есть.
       Подстановка работает только внутри CMS-страниц и писем.

       Поэтому промокод забираем с крошечной служебной страницы:
       1) создайте в GetCourse страницу с адресом promo-bublik,
          доступ — только авторизованным;
       2) единственный блок с содержимым:
          <span id="promoCodeValue">{promo_code_bublik}</span>
       3) адрес и селектор — ниже. */
    promoPageUrl:      '/promo-bublik',
    promoPageSelector: '#promoCodeValue',

    /* Если промокод уже попадает в саму страницу (через код
       оформления или window-переменную) — сработают эти источники,
       и запрос со страницы выше даже не понадобится. */
    /* &#9888;&#65039; 'input.source-code-to-link' ОТСЮДА УБРАН (08.09.2026).
       Это поле реферальной ССЫЛКИ самого GetCourse, в нём лежит
       '?gcpc=2091a' — партнёрский код аккаунта, а не наш промокод.
       Он подхватывался первым, и ученику показывалось «2091a»
       вместо «bublik…». Замерено на живой странице: со служебной
       страницы /promo-bublik в тот же момент приходил правильный
       код, но до неё дело не доходило. */
    promoSelectors: [
      '#partnerPromoCode',
      '[data-partner-promo]'
    ],
    promoGlobalVar: 'egenPromoCode',

    /* СОХРАНЕНИЕ ВЫБОРА. Прежний запрос в /pl/api/users/<id>
       работать не может: это серверный API, он требует секретный
       ключ аккаунта, сессия и csrfToken ему не подходят.
       Рабочий путь — блок-форма GetCourse на странице /save-payout.

       &#9888;&#65039; ОБА ЧИСЛА СНЯТЫ С ЖИВОЙ ФОРМЫ 08.09.2026, а не выведены
       из названий полей:
         action формы      → .../block-public/process?id=2248656884
         единственное поле → formParams[userCustomFields][17226572]
       GetCourse ждёт НОМЕР пользовательского поля, а не его имя.

       &#9888;&#65039; ИСТОРИЯ, ЧТОБЫ НЕ ПОВТОРЯТЬ. До 08.09 форма была привязана
       к полю 17041369 «payout_option» типа ЧИСЛОВОЕ. Отправка
       проходила и отвечала «Спасибо, форма отправлена», а в профиле
       не появлялось ничего: в числовое поле строка не кладётся.
       Рядом жило второе поле 17041373 «РЕФ способ вывода» типа
       СПИСОК — тоже не подходило: список принимает только свои
       значения побуквенно. Оба признаны устаревшими и заменены на
       новое поле типа СТРОКА (17226572), которое принимает любой
       текст. Отсюда правило: тип поля решает не меньше, чем номер.

       &#9888;&#65039; Если форму на /save-payout пересоберут или поле поменяют,
       оба числа изменятся, и сохранение молча перестанет работать.
       Проверяется за минуту: открыть /save-payout и посмотреть
       action формы и name её поля. */
    formId:    2248656884,
    fieldId:   17226572,
    formPage:  '/save-payout'
  };

  /* ---------- 1. Кому и где показывать ---------- */

  var qs = location.search;
  if (/[?&]original=1/.test(qs)) { return; }

  var isOwnCabinet =
    (window.controllerId === 'control/participant' && window.actionId === 'me') ||
    /^\/sales\/control\/participant\/me(\/|$)/.test(location.pathname);

  if (!isOwnCabinet) { return; }   // admin: index, user, settings, payoff и т.д. — не трогаем

  if (document.querySelector('.custom-partner-cabinet')) { return; }

  // Проверка на админа делается позже, в whenReady: GetCourse грузит
  // код оформления и бандлом из <head>, когда .gc-user-admin ещё не
  // существует в DOM — проверка «на старте» молча не срабатывала.
  function isAdminNow() {
    return !!document.querySelector('.gc-user-admin');
  }

  /* ---------- 2. Прячем старое только на время сборки ---------- */

  var root = document.documentElement;
  root.classList.add(CFG.cls + '-loading');
  var failsafe = setTimeout(giveUp, CFG.timeoutMs);

  function giveUp() {
    clearTimeout(failsafe);
    root.classList.remove(CFG.cls + '-loading');   // показываем оригинал, а не пустоту
  }

  /* ---------- 3. Утилиты ---------- */

  function whenReady(fn) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', fn);
    } else { fn(); }
  }

  function poll(test, ok, fail, tries, delay) {
    (function step(n) {
      if (test()) { return ok(); }
      if (n <= 0) { return fail(); }
      setTimeout(function () { step(n - 1); }, delay);
    })(tries);
  }

  /* &#9888;&#65039; ПРАВКА 1 ИЗ ТРЁХ. Амперсанд записан кодом \u0026, а не
     символом. Редактор темы GetCourse раскодирует HTML-сущности
     при сохранении: сущность апострофа превращалась в апостроф
     внутри апострофов, и файл переставал парситься ЦЕЛИКОМ.
     \u0026 — тот же символ, но собирается уже в браузере,
     поэтому в исходнике нет ни одной сущности, которую можно
     было бы раскодировать.
     Проверено исполнением: на входе  a & b < c > d " e ' f
     на выходе получаются правильные HTML-сущности. */
  function esc(s) {
    return String(s).replace(/[&<>"']/g, function (c) {
      return {
        '&': '\u0026amp;',
        '<': '\u0026lt;',
        '>': '\u0026gt;',
        '"': '\u0026quot;',
        "'": '\u0026#39;'
      }[c];
    });
  }

  /* Реальные строки #userStat в GetCourse (проверено на живой странице):
       0  Сумма комиссий партнера
       1  Сумма комиссий учителя
       2  Сумма комиссий всего        -> «Получено»
       3  Выплачено                   -> «Уже использовано»
       4  Осталось выплатить          -> «Доступно»
     Старый код брал [3], [4], [5]: [5] не существует, поэтому
     «Доступно» всегда показывало 0, а две другие были сдвинуты.
     Ищем по подписи строки, индекс — только запасной вариант.
     textContent, а не innerText: innerText зависит от отрисовки. */
  function money(labels, fallbackIdx) {
    var box = document.getElementById('userStat');
    if (!box) { return '0 руб.'; }
    var rows = box.querySelectorAll('tr');
    for (var i = 0; i < rows.length; i++) {
      var t = String(rows[i].textContent || '').toLowerCase().replace(/\s+/g, ' ');
      for (var j = 0; j < labels.length; j++) {
        if (t.indexOf(labels[j]) !== -1) {
          var v = rows[i].querySelector('.money-value');
          if (v) { return String(v.textContent || '').trim() || '0 руб.'; }
        }
      }
    }
    var all = box.querySelectorAll('.money-value');
    return all[fallbackIdx] ? String(all[fallbackIdx].textContent || '').trim() : '0 руб.';
  }

  // неподставленная переменная вида {promo_code_bublik} — это не промокод
  function cleanPromo(raw) {
    raw = String(raw || '').replace('?gcpc=', '').trim();
    return /^\{.*\}$/.test(raw) ? '' : raw;
  }

  function findPromo() {
    for (var i = 0; i < CFG.promoSelectors.length; i++) {
      var el = document.querySelector(CFG.promoSelectors[i]);
      if (el) {
        var raw = cleanPromo(el.value !== undefined ? el.value : el.textContent);
        if (raw) { return raw; }
      }
    }
    return cleanPromo(window[CFG.promoGlobalVar]);
  }

  // промокод со служебной CMS-страницы (там переменная подставляется)
  function fetchPromo(cb) {
    if (!CFG.promoPageUrl) { return cb(''); }
    fetch(CFG.promoPageUrl, { credentials: 'same-origin' })
      .then(function (r) { return r.ok ? r.text() : ''; })
      .then(function (t) {
        if (!t) { return cb(''); }
        var doc = new DOMParser().parseFromString(t, 'text/html');
        var el = doc.querySelector(CFG.promoPageSelector);
        cb(el ? cleanPromo(el.textContent) : '');
      })
      .catch(function () { cb(''); });
  }

  function copyText(text, done) {
    function legacy() {
      var ta = document.createElement('textarea');
      ta.value = text;
      ta.setAttribute('readonly', '');
      ta.style.cssText = 'position:fixed;top:-1000px;opacity:0';
      document.body.appendChild(ta);
      ta.select();
      var ok = false;
      try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
      document.body.removeChild(ta);
      done(ok);
    }
    if (navigator.clipboard && window.isSecureContext) {
      navigator.clipboard.writeText(text).then(function () { done(true); }, legacy);
    } else { legacy(); }
  }

  /* ---------- 4. Старт: ждём AJAX-статистику ---------- */

  whenReady(function () {
    if (CFG.skipAdmins && isAdminNow() && !/[?&]student=1/.test(qs)) { return giveUp(); }
    // GetCourse делает $('#userStat').load('/pl/sales/participant/user-stats?userId=...')
    // уже после DOMContentLoaded — поэтому ждём появления строк
    poll(statsReady, build, function () { build(); }, 26, 300);
  });

  function statsReady() {
    var box = document.getElementById('userStat');
    return !!(box && box.querySelector('.money-value'));
  }

  /* ---------- 5. Сборка ---------- */

  function build() {
    // GetCourse подключает файл оформления дважды — отдельно и в общем
    // бандле, так что этот код выполняется в двух экземплярах.
    // Второй просто отходит в сторону, погасив свой таймер.
    if (document.querySelector('.custom-partner-cabinet')) {
      clearTimeout(failsafe);
      root.classList.remove(CFG.cls + '-loading');
      return;
    }

    var promo = findPromo();

    var data = {
      promo:     promo,
      earned:    money(['сумма комиссий всего'], 2),
      used:      money(['выплачено'], 3),
      available: money(['осталось выплатить'], 4)
    };

    /* Вставляем СОСЕДОМ со старой формой, а не внутрь неё.
       document.body в качестве якоря использовать нельзя:
       insertAdjacentHTML('beforebegin') у body кладёт узел вне body. */
    var anchor = document.getElementById('participantForm') ||
                 document.querySelector('.page-header') ||
                 document.querySelector('.main-page-container > .row');

    if (!anchor || !anchor.parentNode) { return giveUp(); }

    anchor.insertAdjacentHTML('beforebegin', template(data));

    var cabinet = document.querySelector('.custom-partner-cabinet');
    if (!cabinet) { return giveUp(); }

    clearTimeout(failsafe);
    root.classList.add(CFG.cls);
    root.classList.remove(CFG.cls + '-loading');

    bindSelect(cabinet);

    if (promo) {
      showPromo(cabinet, promo);
    } else {
      fetchPromo(function (code) {
        if (code) { showPromo(cabinet, code); }
        else { setPromoText(cabinet, 'промокод', 'скоро появится'); }
      });
    }
  }

  function template(d) {
    // промокод может приехать чуть позже — сразу рисуем плашку-заглушку
    var promoBox =
      '<div class="promo-code-box is-empty" id="copyPromoBtn">' +
        '<span class="promo-code-label" id="copyLabel">промокод</span>' +
        '<span class="promo-code-value" id="copyValue">' +
          (d.promo ? esc(d.promo) : 'загружаем...') +
        '</span>' +
      '</div>';

    return '' +
    '<div class="custom-partner-cabinet">' +

      '<div class="partner-hero-banner">' +
        '<span class="partner-tag">Твой промокод</span>' +
        /* &#9888;&#65039; ПРАВКА 2 ИЗ ТРЁХ: здесь стояла сущность звёздочки —
           редактор темы может её раскодировать, а потом испортить
           сам символ, как уже было с эмодзи в CSS плашки «важно».
           \u2728 — тот же знак, но кодом. */
        '<div class="partner-hero-title">приглашай друзей — получай бонусы \u2728</div>' +
        '<div class="partner-hero-sub">отправь другу свой персональный промокод — он получит скидку на обучение, а тебе начислятся бонусы</div>' +
        '<div class="partner-promo-row">' +
          promoBox +
          '<div class="promo-badge"><div class="promo-badge-val">-300 ₽</div><div class="promo-badge-lbl">скидка другу</div></div>' +
          '<div class="promo-badge"><div class="promo-badge-val">+300</div><div class="promo-badge-lbl">бонусов тебе</div></div>' +
        '</div>' +
        '<a href="' + CFG.rulesUrl + '" target="_blank" rel="noopener" class="rules-link">подробные правила программы →</a>' +
      '</div>' +

      '<div class="partner-section-title">Твои бонусы</div>' +
      '<div class="bonuses-grid">' +
        '<div class="bonus-card pink-card"><div class="bonus-card-lbl">Получено</div>' +
          '<div class="bonus-card-val">' + esc(d.earned) + '</div></div>' +
        '<div class="bonus-card pink-card"><div class="bonus-card-lbl">Уже использовано</div>' +
          '<div class="bonus-card-val">' + esc(d.used) + '</div></div>' +
        '<div class="bonus-card green-card"><div class="bonus-card-lbl">Доступно</div>' +
          '<div class="bonus-card-val">' + esc(d.available) + '</div></div>' +
      '</div>' +

      '<div class="withdrawal-card">' +
        /* &#9888;&#65039; ПРАВКА 3 ИЗ ТРЁХ: здесь стояла сущность подарка.
           Эмодзи вне основного диапазона пишется ПАРОЙ кодов —
           \uD83C\uDF81, это один знак, а не два. */
        '<div class="partner-section-title" style="margin-bottom:5px">Как использовать бонусы? \uD83C\uDF81</div>' +
        '<div class="withdrawal-hint">Выбери, куда хочешь получить накопленные бонусы.</div>' +
        '<select class="withdrawal-select" id="paymentOptionSelect">' +
          /* &#9888;&#65039; ЗНАЧЕНИЯ (value) — ЭТО ТО, ЧТО ЛЯЖЕТ В БАЗУ.
             Поле 17226572 строковое, поэтому принимает любой текст,
             и точного совпадения со справочником больше не требуется.
             Но по этим строкам потом строятся фильтры и выгрузки:
             любое расхождение в регистре или пробеле разведёт одних
             и тех же детей по разным группам. Формулировки ниже
             закреплены 08.09.2026 и МЕНЯТЬ ИХ НЕЛЬЗЯ без правки
             всего, что на них опирается. */
          '<option value="" disabled selected>Выбери вариант</option>' +
          '<option value="сертификат Ozon">сертификат Ozon</option>' +
          '<option value="сертификат Золотое яблоко">сертификат Золотое яблоко</option>' +
          '<option value="бонусы на обучение">бонусы на обучение</option>' +
        '</select>' +
        '<div class="save-status-msg" id="saveStatus"></div>' +
      '</div>' +

    '</div>';
  }

  /* ---------- 6. Поведение ---------- */

  function setPromoText(cabinet, label, value) {
    var l = cabinet.querySelector('#copyLabel');
    var v = cabinet.querySelector('#copyValue');
    if (l) { l.textContent = label; }
    if (v) { v.textContent = value; }
  }

  // промокод приехал — оживляем плашку
  function showPromo(cabinet, promo) {
    var btn = cabinet.querySelector('#copyPromoBtn');
    if (!btn) { return; }
    btn.classList.remove('is-empty');
    btn.setAttribute('role', 'button');
    btn.setAttribute('tabindex', '0');
    setPromoText(cabinet, 'нажми, чтобы скопировать', promo);
    bindCopy(cabinet, promo);
  }

  function bindCopy(cabinet, promo) {
    var btn = cabinet.querySelector('#copyPromoBtn');
    var label = cabinet.querySelector('#copyLabel');
    if (!btn || !promo || btn.dataset.copyBound) { return; }
    btn.dataset.copyBound = '1';

    function run() {
      copyText(promo, function (ok) {
        label.textContent = ok ? 'Скопировано!' : 'Скопируй вручную';
        label.style.color = '#B01513';
        setTimeout(function () {
          label.textContent = 'нажми, чтобы скопировать';
          label.style.color = '';
        }, 2000);
      });
    }

    btn.addEventListener('click', run);
    btn.addEventListener('keydown', function (e) {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); run(); }
    });
  }

  function bindSelect(cabinet) {
    var sel = cabinet.querySelector('#paymentOptionSelect');
    var status = cabinet.querySelector('#saveStatus');
    if (!sel) { return; }

    try {
      var saved = localStorage.getItem('egen_payment_option');
      if (saved) { sel.value = saved; }
    } catch (e) { /* приватный режим */ }

    sel.addEventListener('change', function () {
      var value = this.value;
      try { localStorage.setItem('egen_payment_option', value); } catch (e) {}

      status.style.display = 'block';
      status.textContent = 'Сохраняем...';

      savePaymentOption(value, function (ok) {
        if (ok) {
          status.textContent = 'Сохранено ✓';
          setTimeout(function () { status.style.display = 'none'; }, 2000);
        } else {
          status.textContent = 'Не удалось сохранить — напиши нам в поддержку';
        }
      });
    });
  }

  /* Как включить реальное сохранение:
     1) в GetCourse создать страницу с блоком «Форма», одно поле,
        привязанное к пользовательскому полю payment_option;
     2) в коде блока взять её id (в адресе .../process-html?id=XXXXXX);
     3) вписать его в CFG.formId.
     Пока formId пуст — выбор сохраняется только в браузере ученика,
     но интерфейс при этом не ломается. */
  function savePaymentOption(value, cb) {
    /* &#9888;&#65039; Раньше здесь стояло `if (!CFG.formId) { return cb(true); }` —
       «сохранение прошло успешно», когда сохранять было некуда.
       Ученик видел «Сохранено», а в GetCourse не появлялось ничего.
       Теперь при отсутствии формы честно отвечаем отказом. */
    if (!CFG.formId) { return cb(false); }

    /* Шлём ровно те поля, что шлёт настоящая форма, и тем же
       способом, каким уже работает кнопка CloudText: запрос уходит
       на свой же домен, cookie сессии едут сами, поэтому GetCourse
       понимает, ЧЕЙ профиль обновлять. Почту передавать не нужно. */
    var тело = new URLSearchParams();
    тело.set('formParams[setted_offer_id]', '');
    тело.set('formParams[willCreatePaidDeal]', '0');
    тело.set('__gc__internal__form__helper', window.location.origin + CFG.formPage);
    тело.set('__gc__internal__form__helper_ref', window.location.href);
    тело.set('formParams[userCustomFields][' + CFG.fieldId + ']', value);

    fetch(window.location.origin + '/pl/lite/block-public/process?id=' + CFG.formId, {
      method: 'POST',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'X-Requested-With': 'XMLHttpRequest'
      },
      body: тело.toString()
    }).then(function (r) {
      if (!r.ok) { return null; }
      return r.json().catch(function () { return null; });
    }).then(function (ответ) {
      /* &#9888;&#65039; Успехом считаем только явное подтверждение GetCourse.
         Пустой или неразобранный ответ — это НЕ успех: лучше
         сказать ученику «не удалось», чем снова соврать. */
      cb(!!(ответ && (ответ.success || ответ.formProcessed)));
    }, function () { cb(false); });
  }

})();